import sys
import wx
import os

from operator import attrgetter
from threading import Event, Timer

from action import ActionHandler
from addtorrents import AddTorrents
from ratemanager import RateManager

from BitTornado.__init__ import product_name
from BitTornado.clock import clock

from LMG.Utility.constants import *
from LMG.Utility.helpers import findUniqueFileName
from LMG.GUI.Base.guiupdate import SafeInvocation
from LMG.GUI.Base.ArtManager import ArtManager

################################################################

class Scheduler(SafeInvocation, wx.EvtHandler):
    """
    Determine which torrents need to run, update global stats,
    and deal with loading, moving, and removing torrents.
    """
    def __init__(self):
        wx.EvtHandler.__init__(self)
        SafeInvocation.__init__(self)
        
        utility.queue = self
        
        utility.actionhandler = ActionHandler()
        self.ratemanager = RateManager()
        self.addtorrents = AddTorrents()

        self.timers = {}
        
        self.doneflag = Event()

        self.totals = { 'up' : 0.0, 
                        'down' : 0.0, 
                        'connections': 0 }

        self.totals_kb = { 'up': 0.0, 
                           'down': 0.0 }

        self.volume = {'up': utility.config.Read('uploadvolume', "int"),
                       'down': utility.config.Read('downloadvolume', "int") }
        
        self.whenidle = 0   # 0=Nothing, 1=Close, 2=Shutdown
        self.lastDirScan = 0
        self.lastScrape = 0
        
        self.UpdateRunningTorrentCounters()
        
    def postInitTasks(self):
        # Read old list from torrent.lst
        ####################################
        self.addtorrents.readTorrentList()

        # Wait until after creating the list and adding torrents
        # to start CyclicalTasks in the scheduler
        self._CyclicalTasks()
      
    def _CalculateTorrentCounters(self):
        """
        Update the counters for torrents in a single unified place
        """
        torrents_active = utility.torrents["active"].keys()

        paused = {}
        seeding = {}
        downloading = {}
                                                   
        for torrent in torrents_active:
            # Torrent is active
            if (torrent.status.value == STATUS_HASHCHECK):
                activevalues = [ STATUS_ACTIVE, STATUS_PAUSE, STATUS_SUPERSEED ]
                # Torrent is doing a hash check
                # (Count towards counters if it was active before the the check,
                #  otherwise don't)
                if not torrent.actions.oldstatus in activevalues:
                    continue
            
            if torrent.status.value == STATUS_PAUSE:
                paused[torrent] = 1
            
            
            if torrent.status.completed:
                seeding[torrent] = 1
            else:
                downloading[torrent] = 1

        utility.torrents["paused"] = paused
        utility.torrents["seeding"] = seeding
        utility.torrents["downloading"] = downloading
    
    def getProcCount(self):
        return len(utility.torrents[self.getActiveGroup()])
    
    def getActiveGroup(self):
        groups = ("downloading", "seeding", "active")
        consideractive = utility.config.Read('consideractive', "int")
        consideractive = max(min(consideractive, 2), 0)
        return groups[consideractive]
        
    def UpdateRunningTorrentCounters(self):
        self._CalculateTorrentCounters()

    def _getDownUpConnections(self):
        """
        Ask UD/DL speed of all threads
        """
        totalupload     = 0.0
        totaldownload   = 0.0

        for torrent in utility.torrents["active"].keys():
            if torrent.status.value != STATUS_PAUSE:
                totalupload += torrent.getColumnValue(COL_ULSPEED)
                totaldownload += torrent.getColumnValue(COL_DLSPEED)
                
        self.totals['up'] = totalupload
        self.totals_kb['up'] = (totalupload / 1024.0)
        
        self.totals['down'] = totaldownload
        self.totals_kb['down'] = (totaldownload / 1024.0)
            
    def getSpeed(self, dir):
        maxrate = self.ratemanager.MaxRate(dir)
        if maxrate == 0:
            return utility.speed_format(self.totals[dir], truncate = 1)
        speed = utility.size_format(self.totals[dir], truncate = 1, stopearly = "KB", applylabel = False)
        ratecap = utility.speed_format((maxrate * 1024), truncate = 0, stopearly = "KB")
        return speed + " / " + ratecap
        
    def _updateTrayAndStatusBar(self):
        self.invokeLater(self._onUpdateTrayAndStatusBar)

    def _onUpdateTrayAndStatusBar(self):
        uploadspeed = self.getSpeed("up")
        downloadspeed = self.getSpeed("down")
        
        
        try:
            # update value in minimize icon
            ###########################################
            if utility.frame.tbicon and utility.frame.tbicon.IsIconInstalled():
                icontext = product_name + "\n\n" + \
                           _('DL:') + " " + downloadspeed + "\n" + \
                           _('UL:') + " " + uploadspeed + " "
                utility.frame.tbicon.setIcon(icontext)

            # update in status bar
            ##########################################
            if hasattr(utility.frame, "StatusBar") and utility.frame.StatusBar.IsShown():
                utility.frame.StatusBar.updateFields()

            # update speed graph
            ##########################################
            utility.window.details.updateGraph(self.totals_kb['up'], self.totals_kb['down'])
            
        except wx.PyDeadObjectError:
            pass
                                
    def _CyclicalTasks(self):       
        self._getDownUpConnections()
            
        self._updateTrayAndStatusBar()

        self.ratemanager.RunTasks()
   
        try:
            # Run postponed deleting events
            while utility.window.postponedevents:
                ev = utility.window.postponedevents.pop(0)
                #print "POSTPONED EVENT : ", ev[0]
                
                # Handle multiple arguments
                funcname = ev.pop(0)
                funcname(*ev)
            utility.window.list.Enable()
        except wx.PyDeadObjectError:
            pass

        # Run Directory Scanner
        freq = utility.config.Read('scandirfreq', "int")
        if utility.config.Read('scandiractive', "boolean") and (self.lastDirScan == 0 or clock() - self.lastDirScan > freq):
            self.invokeLater(self._ScanDir)
            self.lastDirScan = clock()
        
        # Scrape Queued Torrents
        if utility.config.Read('scrapequeued', "boolean") and (self.lastScrape == 0 or clock() - self.lastScrape > 300):
            self.invokeLater(self._ScrapeQueuedTorrents)
            self.lastScrape = clock()
            
        # Try invoking the scheduler
        # (just in case we need to start more stuff:
        #  should return almost immediately otherwise)
        self.invokeLater(self._Scheduler)

        # Start Timer
        ##########################################
        self.timers['frequent'] = Timer(2, self._CyclicalTasks)
        self.timers['frequent'].start()
               
    def updateAndInvoke(self, updateCounters = True, invokeLater = True):
        if updateCounters:
            # Update counter for running torrents
            self.UpdateRunningTorrentCounters()
        # Only invoke the scheduler if we're not shutting down
        if invokeLater:
            self.invokeLater(self._Scheduler)
            self.invokeLater(self.checkIdleForClosing)
      
    def _updateTorrentList(self):
        torrentconfig = utility.torrentconfig
       
        maxindex = len(utility.torrents["all"])
        items = utility.torrentconfig.Items()
        
        # Delete all the items beyond the torrents that exist
        if len(items) > maxindex:
            for index, src in items:
                try:
                    if int(index) >= maxindex:
                        torrentconfig.DeleteEntry(index)
                except:
                    # (Got an index that wasn't a number?)
                    torrentconfig.DeleteEntry(index)
       
        for torrent in utility.torrents["all"]:
            torrent.torrentconfig.writeSrc(False)

        torrentconfig.Flush()
        
    def getInactiveTorrents(self, numtorrents, completeOnly = False, incompleteOnly = False):
        if numtorrents < 0:
            numtorrents = 0

        torrents_inactive = utility.torrents["inactive"].keys()

        # Find which torrents are queued:
        if completeOnly:
            inactivetorrents = [torrent for torrent in torrents_inactive if (torrent.status.completed and torrent.status.value == STATUS_QUEUE)]
        elif incompleteOnly:
            inactivetorrents = [torrent for torrent in torrents_inactive if (not torrent.status.completed and torrent.status.value == STATUS_QUEUE)]            
        else:
            inactivetorrents = [torrent for torrent in torrents_inactive if (torrent.status.value == STATUS_QUEUE)]
        
        inactivelength = len(inactivetorrents)

        if inactivelength > numtorrents:
            # Sort by listindex
            inactivetorrents.sort(key = attrgetter('listindex'))
            # Sort by seeding/downloading
            if utility.config.Read('preferuncompleted', "boolean"):
                inactivetorrents.sort(key = lambda x:x.status.completed)
            # Sort by priority
            inactivetorrents.sort(key = attrgetter('prio'))
                
            # Slice off the number of torrents we need to start
            inactivetorrents = inactivetorrents[0:numtorrents]
                
        return inactivetorrents
    
    def getActiveTorrents(self, numtorrents):
        if numtorrents < 0:
            numtorrents = 0

        activetorrents = [torrent for torrent in utility.torrents["all"] if torrent.status.isActive()]
        activelength = len(activetorrents)

        if activelength > numtorrents:
            # Sort by listindex
            activetorrents.sort(key = attrgetter('listindex'))
            # Sort by seeding/downloading
            if utility.config.Read('preferuncompleted', "boolean"):
                activetorrents.sort(key = lambda x:x.status.completed)                
            # Sort by priority
            activetorrents.sort(key = attrgetter('prio'))

            # Worst first
            activetorrents.reverse()
            
            # Slice off the number of torrents we need
            activetorrents = activetorrents[0:numtorrents]
                
        return activetorrents
        

    def _ScanDir(self):
        self.scandir = utility.config.Read('scandir')

        if not utility.config.Read('setdefaultfolder', "boolean"):
            error = _("Could not start Directory Scanner, you must set a default download folder")
        elif not os.path.isdir(self.scandir):
            error = _("Could not start Directory Scanner, the scan folder is not a directory")
        elif self.scandir == os.path.join(utility.getConfigPath(), 'torrent'):
            error = _("Could not start Directory Scanner, you can't use the default torrent directory")
        else:
            error = None
        if error:
            wx.LogError(error)
            utility.config.Write('scandiractive', "0")
            utility.actions[ACTION_DIRSCANNER].updateButton()
            return

        scanned = [entry for entry in os.listdir(self.scandir) if os.path.isfile(os.path.join(self.scandir, entry))
                   and os.access(os.path.join(self.scandir, entry), os.R_OK)
                   and not entry.startswith(".")
                   and (entry.endswith(".torrent") or entry.endswith(".txt")
                        or os.path.getsize(os.path.join(self.scandir, entry)) < 1024*25)]

        if scanned:
            scanneddir = os.path.join(self.scandir, 'scanned')
            baddir = os.path.join(self.scandir, 'bad')
            
            for file in scanned:
                src = os.path.join(self.scandir, file)
                try:
                    dump1, msg, dump2 = utility.queue.addtorrents.AddTorrentFromFile(src, dotTorrentDuplicate = True, caller = "web")
                except:
                    continue
                    
                if msg == _('OK'):
                    if utility.config.Read('scandirmovetor', "boolean"):
                        moveto = scanneddir
                    else:
                        moveto = None
                    wx.LogMessage('Directory Scanner: %s loaded' % (src))
                elif file.endswith(".torrent"):
                    moveto = baddir
                else:
                    continue
                
                # Move file
                if moveto is None:
                    try:
                        os.remove(src)
                    except:
                        pass
                    continue
                elif not os.path.exists(moveto):
                    os.makedirs(moveto)
                elif os.path.isfile(moveto):
                    continue
                dest = findUniqueFileName(os.path.join(moveto, file), 'scan')
                if dest is None:
                    continue
                try:
                    os.rename(src, dest)
                except:
                    pass

    def _ScrapeQueuedTorrents(self):
        for torrent in utility.torrents["all"]:
            if torrent.status.value == STATUS_QUEUE:
                # Scrape queued torrents every hour
                torrent.actions.scrape(interval = 60)
        
    def _Scheduler(self):
        """
        Find new processes to start
        """
        if self.doneflag.isSet():
            return
        self.doneflag.set()

        # Max upload volume
        maxuploadvolume = utility.config.Read('maxuploadvolume', "int")
        uploadvolume    = self.volume['up'] / 1048576.0
        if maxuploadvolume > 0 and uploadvolume >= maxuploadvolume:
            toqueue = [torrent for torrent in utility.torrents["all"] if torrent.status.isActive()]
            utility.actionhandler.procQUEUE(toqueue)
            self.UpdateRunningTorrentCounters()
            self.doneflag.clear()
            return
        
        # Max download volume
        maxdownloadvolume = utility.config.Read('maxdownloadvolume', "int")
        downloadvolume    = self.volume['down'] / 1048576.0
        if maxdownloadvolume > 0 and downloadvolume >= maxdownloadvolume:
            toqueue = [torrent for torrent in utility.torrents["all"] if torrent.status.isActive() and not torrent.status.completed]
            utility.actionhandler.procQUEUE(toqueue)
            self.UpdateRunningTorrentCounters()
            self.doneflag.clear()
            return
        
        inactivestarted = 0

        # Start all completed torrents if we only consider downloading torrents as active
        if self.getActiveGroup() == "downloading":
            # Get all inactive torrents that are complete
            inactivetorrents = self.getInactiveTorrents(len(utility.torrents["inactive"]), completeOnly = True)
            
            # Start them if they're not already started
            for torrent in inactivetorrents:
                if torrent.actions.resume():
                    inactivestarted += 1
                    
            if inactivestarted > 0:
                self.UpdateRunningTorrentCounters()
                
        # Start all incompleted torrents if we only consider seeding torrents as active
        elif self.getActiveGroup() == "seeding":
            # Get all inactive torrents that are incomplete
            inactivetorrents = self.getInactiveTorrents(len(utility.torrents["inactive"]), incompleteOnly = True)
            
            # Start them if they're not already started
            for torrent in inactivetorrents:
                if torrent.actions.resume():
                    inactivestarted += 1
                    
            if inactivestarted > 0:
                self.UpdateRunningTorrentCounters()
        
        numsimdownload = utility.config.Read('numsimdownload', "int")
            
        # Max number of torrents to start
        torrentstostart = numsimdownload - self.getProcCount()
        if torrentstostart > 0:
            inactivestarted = 0
                
            # Start torrents
            inactivetorrents = self.getInactiveTorrents(torrentstostart)
                               
            for torrent in inactivetorrents:
                if torrent.actions.resume():
                    inactivestarted += 1
                        
            torrentstostart = torrentstostart - inactivestarted
            
            if inactivestarted > 0:
                self.UpdateRunningTorrentCounters()
        
        self.doneflag.clear()
        
    def checkIdleForClosing(self):
        if self.whenidle and not utility.torrents["active"]:
            # Wait 60 seconds before closing
            messages = (0, _("Closing Client"), _("Closing Client And Shutting Computer Down"))
            delacdlg = wx.ProgressDialog(messages[self.whenidle], _('The client is idle and will close soon'),
                                         maximum = 240, parent = utility.frame,
                                         style = wx.PD_CAN_ABORT | wx.PD_AUTO_HIDE)
            keepgoing = True
            count = 0
            while keepgoing and count < 240:
                count += 1
                wx.MilliSleep(250)
                keepgoing = delacdlg.Update(count)
                if type(keepgoing) is tuple:
                    keepgoing = keepgoing[0]
            delacdlg.Destroy()
            
            if keepgoing:
                # Close
                self.invokeLater(utility.frame.OnCloseWindow, kwargs = {'silent':True, 'shutdown':self.whenidle==2})
            else:
                # Cancel Closing
                self.whenidle = 0
                utility.actions[ACTION_ONIDLE].CheckNeeded()
      
    def changeABCParams(self):
        for torrent in utility.torrents["all"]:
            #Local doesn't need to affect with change ABC Params
            torrent.connection.resetUploadParams()

        self.updateAndInvoke()

    def clearAllCompleted(self, removelist = None):
        """
        Clear all completed torrents from the list

        Passing in a list of torrents to remove + move
        allows for a torrent to auto-clear itself when
        completed
        """
        if removelist is None:
            removelist = [torrent for torrent in utility.torrents["inactive"].keys() if torrent.status.isDoneUploading()]

        # See if we need to move the completed torrents
        # before we remove them from the list
        if utility.config.Read('movecompleted', "boolean") and utility.config.Read('defaultmovedir') and \
           utility.config.Read('movecompletedonclear', "boolean"):
            utility.actionhandler.procMOVE(removelist)
        
        # Remove the torrents
        utility.actionhandler.procREMOVE(removelist)
            
    def clearScheduler(self):       
        # Stop frequent timer
        try:
            if self.timers['frequent'] is not None:
                self.timers['frequent'].cancel()
        except:
            pass

        torrents_inactive = utility.torrents["inactive"].keys()

        # Call shutdown on inactive torrents
        # (controller.stop will take care of the rest)
        for torrent in torrents_inactive:
            torrent.shutdown()

        # Stop all active torrents
        utility.controller.stop()

        # Store volume
        utility.config.Write('uploadvolume', self.volume['up'], "int")
        utility.config.Write('downloadvolume', self.volume['down'], "int")
        
        # Stop DHT
        if utility.dht:
            utility.dht.close()
        
        # Update the torrent list
        self._updateTorrentList()
            
    def getTorrent(self, index = -1, info_hash = None):
        """
        Look for a torrent in the list, either by its index
        or by its infohash
        """
        # Find the hash by the index
        if index >= 0 and index <= len(utility.torrents['all']):
            inAllIndex = utility.window.list.GetItemData(index)
            return  utility.torrents["all"][inAllIndex]
            
        # Can't find it by index and the hash is none
        # We're out of luck
        if info_hash is None:
            return None

        # Look for the hash value
        for torrent in utility.torrents["all"]:
            if torrent.infohash == info_hash:
                return torrent

    def sortList(self, colid = 0, reverse = False):
        utility.torrents["all"].sort(key = lambda x: x.getColumnValue(colid, -1.0), reverse = reverse)
        self.updateListIndex()

    def updateListIndex(self, startindex = 0, endindex = None):
        if utility.activeGroup != "all":
            return
        
        # Can't update indexes for things that aren't in the list anymore
        if startindex >= len(utility.torrents["all"]):
            return

        if startindex < 0:
            startindex = 0
        if endindex is None or endindex >= len(utility.torrents["all"]):
            endindex = len(utility.torrents["all"]) - 1

        for i in range(startindex, endindex + 1):
            torrent = utility.torrents["all"][i]
            torrent.changeListIndex(i)
            torrent.status.dontupdate = False
            torrent.updateColumns(force = True)
            torrent.torrentconfig.writeSrc(False)
        utility.torrentconfig.Flush()
        utility.window.list.updateDetails()
        ArtManager.Get().MakeAlternateList(utility.window.list)

    def filterGroups(self, group):
        for torrent in utility.torrents["all"]:
            torrent.status.dontupdate = True

        utility.window.list.setNumberOfItems(len(utility.torrents[group]))
        
        newindex = 0
        for torrent in utility.torrents[group]:
            torrent.changeListIndex(newindex)
            torrent.status.dontupdate = False
            torrent.updateColumns(force = True)
            newindex += 1
        
        utility.activeGroup = group
        print group        
        ArtManager.Get().MakeAlternateList(utility.window.list)
        
    def MoveItems(self, listtomove, direction = 1):
        listtomove.sort()
        
        if direction == 1:
            listtomove.reverse()
            startoffset = -1
            endoffset = 0
        else:
            direction = -1
            startoffset = 0
            endoffset = 1
        newloc = []
        for index in listtomove:
            if (direction == 1) and (index == len(utility.torrents["all"]) - 1):
                newloc.append(index)
            elif (direction == -1) and (index == 0):
                newloc.append(index)
            elif newloc.count(index + direction) != 0 :
                newloc.append(index)
            else:
                torrent = utility.torrents["all"].pop(index)
                utility.torrents["all"].insert(index + direction, torrent)
                newloc.append(index + direction)
        if newloc:
            newloc.sort()
            start = newloc[0] + startoffset
            end = newloc[-1] + endoffset
            self.updateListIndex(startindex = start, endindex = end)
            
        return newloc
    
    def MoveItemsTop(self, selected):
        for index in selected:
            if index != 0:       # First Item can't move up anymore
                torrent = utility.torrents["all"].pop(index)
                utility.torrents["all"].insert(0, torrent)               

        if selected:
            self.updateListIndex(startindex = 0, endindex = selected[0])
        
        return True
        
    def MoveItemsBottom(self, selected):
        for index in selected:
            if index < len(utility.torrents["all"]) - 1:
                torrent = utility.torrents["all"].pop(index)
                utility.torrents["all"].append(torrent)
                
        if selected:
            self.updateListIndex(startindex = selected[0])
        
        return True
        
    def addTorrentFromFileCallback(self, *args, **kwargs):
        self.invokeLater(self.addtorrents.AddTorrentFromFile, args, kwargs)
        
    def addTorrentFromMetainfoCallback(self, *args, **kwargs):
        self.invokeLater(self.addtorrents.AddTorrentFromMetainfo, args, kwargs)
